Skip to content

fix(macos/ime): skip key-equivalent priority path while IME is composing (#9709)#9711

Open
maxmilian wants to merge 1 commit intowarpdotdev:masterfrom
maxmilian:fix/9709-ime-arrow-key-double-dispatch
Open

fix(macos/ime): skip key-equivalent priority path while IME is composing (#9709)#9711
maxmilian wants to merge 1 commit intowarpdotdev:masterfrom
maxmilian:fix/9709-ime-arrow-key-double-dispatch

Conversation

@maxmilian
Copy link
Copy Markdown

Description

Fixes #9709 — on macOS, third-party IMEs that listen on the input context (e.g. 超注音 / Yahoo Bopomofo) observed every arrow keystroke twice during candidate selection, advancing the highlight by 2 instead of 1.

Root cause

Arrow keys carry the function-key flag (modifierFlags=0xa00100), so they qualify as key equivalents and AppKit dispatches them through performKeyEquivalent: before keyDown:. In crates/warpui/src/platform/mac/objc/window.m::performKeyEquivalent:, the previous implementation:

  1. Called [self.contentView keyDownImpl:event] — which invokes interpretKeyEvents: and forwards to Rust
  2. Rust suppresses keystrokes during composition (is_composing=true), so keyDownImpl returned NO
  3. Fell through to [super performKeyEquivalent:event]
  4. AppKit then dispatched keyDown: on the host view → keyDownImpl again → interpretKeyEvents ran a second time

Apple's built-in IME uses IMKCandidates (a separate NSPanel) for candidate selection, so the second delivery doesn't reach its candidate index. 超注音 draws its candidate panel itself and listens on the input context directly — it observes both deliveries and advances by 2.

Fix

Skip the priority path in performKeyEquivalent: while hasMarkedText is YES. AppKit then routes the event through the standard keyDown: chain exactly once.

Testing

NSLog instrumentation (before/after)

I added per-call instrumentation around keyDownImpl, interpretKeyEvents, setMarkedText:, insertText:, unmarkText, doCommandBySelector:, and performKeyEquivalent: and reproduced with 超注音 (Yahoo Bopomofo) on macOS 26.4.1.

Before the fix — single right-arrow press during candidate selection:

performKeyEquivalent: keyCode=124 modifiers=0xa00100
performKeyEquivalent: keystrokeIsAssigned=1
keyDownImpl#8 ENTER keyCode=124 wasComposing=1
  interpretKeyEvents BEGIN                    ← IME +1
  setMarkedText: '...' (interpretingKeyEvents=1)
  setMarkedText: '...' (interpretingKeyEvents=1)
  interpretKeyEvents END (hasMarkedText=1)
warp_handle_view_event(composing=1) handled=0
performKeyEquivalent: keyDownImpl returned NO, falling through to super
keyDown: keyCode=124                          ← AppKit fires again
keyDownImpl#9 ENTER keyCode=124 wasComposing=1
  interpretKeyEvents BEGIN                    ← IME +1 again → total +2
  ...

After the fix:

performKeyEquivalent: keyCode=124 modifiers=0xa00100
performKeyEquivalent: IME composing, skipping priority path
keyDown: keyCode=124
keyDownImpl#22 ENTER keyCode=124 wasComposing=1
  interpretKeyEvents BEGIN                    ← single delivery
  interpretKeyEvents END (hasMarkedText=1)

Manual verification (macOS)

  • 超注音 (Yahoo Bopomofo): right/left arrow now advances candidate by exactly 1 per press (was 2). ✅
  • Apple built-in 注音 (Bopomofo): no regression — candidate window behaves identically. ✅
  • Apple Pinyin: no regression. ✅
  • Cmd-shortcut keys without IME composition (e.g. Cmd-T new tab): no regression — the hasMarkedText guard only triggers during active composition, so the priority path still executes for ordinary key equivalents. ✅
  • Plain typing without an IME: no regression. ✅

No automated tests were added: the bug requires running an IME on macOS and observing AppKit's two-stage event delivery (performKeyEquivalent:keyDown:), which is impractical to simulate in the existing unit/integration test harnesses. The change is small, well-scoped (performKeyEquivalent: only), and the guard short-circuits to [super performKeyEquivalent:] on the same path AppKit would fall back to anyway.

Server API dependencies

  • Is this change necessary to make the client compatible with a desired server API breaking change?
  • Does this change rely on a new server API?
  • Is this change enabling the use of a server API on client channels that rely on the production server?

None — this is a pure native-input-handling change.

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

Changelog Entries for Stable

CHANGELOG-BUG-FIX: macOS: fix third-party IMEs (e.g. 超注音 / Yahoo Bopomofo) advancing candidate selection by 2 per arrow press during composition.

Navigation keys with the function-key modifier flag (e.g. arrow keys,
modifierFlags=0xa00100) qualify as key equivalents on macOS, so AppKit
fires performKeyEquivalent: before keyDown:. The previous implementation
called keyDownImpl from performKeyEquivalent:, which invokes
interpretKeyEvents and forwards to Rust. During IME composition, Rust
suppresses the keystroke and keyDownImpl returns NO, so we fall through
to [super performKeyEquivalent:]. AppKit then dispatches keyDown: to the
host view, and interpretKeyEvents runs a second time.

Most macOS IMEs render their candidate UI through IMKCandidates (a
separate NSPanel), so they only observe the first delivery. 超注音 /
Yahoo Bopomofo, however, draws its candidate panel itself and listens
on the input context directly, so it receives both deliveries and
advances candidate selection by 2 per arrow press.

Skip the priority path entirely while hasMarkedText is YES so AppKit
routes the event through keyDown: exactly once. Confirmed via NSLog
instrumentation: arrow press during composition now produces a single
keyDownImpl / interpretKeyEvents pair instead of two.

Fixes warpdotdev#9709

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cla-bot
Copy link
Copy Markdown

cla-bot Bot commented May 1, 2026

Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have the users @maxmilian on file. In order for us to review and merge your code, each contributor must visit https://cla.warp.dev to read and agree to our CLA. Once you have done so, please comment @cla-bot check to trigger another check.

@github-actions github-actions Bot added the external-contributor Indicates that a PR has been opened by someone outside the Warp team. label May 1, 2026
@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 1, 2026

@maxmilian

I'm starting a first review of this pull request.

You can view the conversation on Warp.

I reviewed this pull request and requested human review from: @vorporeal.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR skips Warp's key-equivalent priority path while macOS marked text is active, allowing AppKit to deliver IME composition keys through the normal keyDown: path exactly once.

Concerns

  • No blocking correctness or security concerns found in the changed lines.

Verdict

Found: 0 critical, 0 important, 0 suggestions

Approve

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@oz-for-oss oz-for-oss Bot requested a review from vorporeal May 1, 2026 05:42
@maxmilian
Copy link
Copy Markdown
Author

@cla-bot check

@cla-bot cla-bot Bot added the cla-signed label May 1, 2026
@cla-bot
Copy link
Copy Markdown

cla-bot Bot commented May 1, 2026

The cla-bot has been summoned, and re-checked this pull request!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed external-contributor Indicates that a PR has been opened by someone outside the Warp team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

macOS IME (超注音 / Yahoo Bopomofo): arrow key advances candidate selection by 2 instead of 1

1 participant